package tools;

import java.io.Closeable;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.LinkedBlockingQueue;
import entity.DefinedRuntimeException;

/**
 * 封装写日志操作的实体类. <br/>
 * 类中的方法不会检查路径是否可用， 调用者请保证传入的路径的父文件夹均存在并可用。 <br/>
 * 对象使用完毕后应当关闭。
 * @author Rex
 *
 */
public class WriteLog implements Closeable {
	
	/**
	 * 文件输出流.
	 */
	private FileWriter writer;
	
	/**
	 * 一个阻塞队列，线程安全.
	 */
	private final LinkedBlockingQueue<DefinedRuntimeException> queue = new LinkedBlockingQueue<>();
	
	/**
	 * 标记是否继续执行后台线程.
	 */
	private volatile boolean run = true;
	
	/**
	 * 后台线程.
	 * 用于获取阻塞队列的头部元素并写出到磁盘文件。
	 */
	private final Thread t = new Thread(() -> start());
	
	/**
	 * 后台线程优先级.
	 * 默认为5（正常优先级）。
	 * @see WriteLog#t
	 */
	private int threadPriority = 5;

	/**
	 * 用于格式化当前系统时间.
	 */
	private final SimpleDateFormat sdf = new SimpleDateFormat(" 'at' yyyy-MM-dd HH:mm:ss");

	/**
	 * 写出日志的路径. 
	 * @see WriteLog#setPath(String)
	 */
	private final String path;

	/**
	 * 每条记录是否添加时间后缀. 默认为true.
	 * @see WriteLog#setAddSuffix(boolean)
	 */
	private boolean addSuffix = true;
	
	/**
	 * 是否在原文件尾部继续写入（为false时，直接替换原文件）.
	 * @see WriteLog#isAppend()
	 */
	private boolean append = true;

	/**
	 * 默认的日志写出路径：
	 * Windows环境下默认为C:\temp\error.log，
	 * Linux/Unix环境下默认为/usr/error.log.
	 * @throws DefinedRuntimeException
	 */
	public WriteLog() throws DefinedRuntimeException {
		if (System.getProperty("os.name").toLowerCase().contains("windows")) {
			// Windows
			File file = new File("C:\\temp");
			if (file.exists() == false) {
				file.mkdir();
			}
			path = "C:\\temp\\error.log";
		} else {
			// Linux/Unix
			path = "/usr/error.log";
		}
		create();
	}
	
	/**
	 * 默认的日志写出路径：
	 * Windows环境下默认为C:\temp\error.log，
	 * Linux/Unix环境下默认为/usr/error.log.
	 * @param addSuffix 是否在每条日志末尾增加时间后缀，默认为true
	 * @throws DefinedRuntimeException
	 */
	public WriteLog(boolean addSuffix) throws DefinedRuntimeException {
		this();
		this.addSuffix = addSuffix;
	}

	/**
	 * 构造器不会检查路径是否可用， 调用者请保证传入的路径存在并可用.
	 * @param path
	 * @throws DefinedRuntimeException
	 */
	public WriteLog(String path) throws DefinedRuntimeException {
		this.path = path;
		create();
	}
	
	/**
	 * 构造器不会检查路径是否可用， 调用者请保证传入的路径存在并可用.
	 * @param path
	 * @param addSuffix 是否在每条日志末尾增加时间后缀，默认为true
	 * @throws DefinedRuntimeException
	 */
	public WriteLog(String path, boolean addSuffix) throws DefinedRuntimeException {
		this(path);
		this.addSuffix = addSuffix;
	}
	
	/**
	 * 构造器不会检查路径是否可用， 调用者请保证传入的路径存在并可用.
	 * @param append 设置写日志时，是否在原文件尾部持续写入（为false时，直接替换原文件）
	 * @param path
	 * @param addSuffix 是否在每条日志末尾增加时间后缀，默认为true
	 * @throws DefinedRuntimeException
	 */
	public WriteLog(boolean append, String path, boolean addSuffix) throws DefinedRuntimeException {
		this.path = path;
		this.append = append;
		this.addSuffix = addSuffix;
		create();
	}

	/**
	 * 得到日志写出路径.
	 * @see WriteLog#path
	 * @return 日志路径
	 */
	public String getPath() {
		return path;
	}
	
	/**
	 * 得到后台线程优先级.
	 * @return
	 */
	public int getThreadPriority() {
		return threadPriority;
	}

	/**
	 * 写日志时，是否在原文件尾部继续写入（为false时，直接替换原文件）.
	 * 此值在构造WriteLogOperation对象时确定，之后不得修改。
	 * @return
	 */
	public boolean isAppend() {
		return append;
	}

	/**
	 * 设置后台线程优先级.
	 * 默认为5（正常优先级）。
	 * 注意：此方法必须在首次调用writeAsyn方法之前调用，否则无法生效。
	 * @param threadPriority
	 */
	public void setThreadPriority(int threadPriority) {
		this.threadPriority = threadPriority;
	}

	/**
	 * 得到是否添加时间后缀的布尔值.
	 * @see WriteLog#addSuffix
	 * @return
	 */
	public boolean isAddSuffix() {
		return addSuffix;
	}

	/**
	 * 设置是否添加时间后缀.
	 * @see WriteLog#addSuffix
	 * @param addSuffix
	 */
	public void setAddSuffix(boolean addSuffix) {
		this.addSuffix = addSuffix;
	}
	
	/**
	 * 真正将消息写出到磁盘的方法.
	 * 此方法只负责写字符串。
	 * @param msg 此方法不判空，调用者应保证传入的字符串不为空
	 * @throws DefinedRuntimeException
	 */
	private void writeDown(String msg) throws DefinedRuntimeException {
		try {
			// 若有此文件，则直接使用；若无此文件，则先新建
			writer.write(msg);
			writer.flush();
		} catch (IOException e) {
			// 只抛出异常，不写日志
			throw new DefinedRuntimeException(false, "写出日志失败，文件输出流异常", e);
		} catch (Exception e) {
			// 只抛出异常，不写日志
			throw new DefinedRuntimeException(false, "写日志遭遇未知异常", e);
		}
	}

	/**
	 * 写出日志到磁盘文件.
	 * @param log
	 * @throws DefinedRuntimeException
	 */
	public void write(String log) throws DefinedRuntimeException {
		writeDown(new StringBuilder(log == null ? "(null)" : log).append(addSuffix ? sdf.format(new Date()) : "")
				.append("\r\n").toString());
	}
	
	/**
	 * 写出日志到磁盘文件.
	 * @param e
	 * @throws DefinedRuntimeException
	 */
	public void write(DefinedRuntimeException e) throws DefinedRuntimeException {
		writeDown(getInfo(e));
	}

	/**
	 * 写出日志到磁盘文件（异步）.
	 * @param ex
	 * @throws DefinedRuntimeException
	 */
	public void writeAsyn(DefinedRuntimeException ex) throws DefinedRuntimeException {
		try {
			// 将待写出的消息保存在队列
			queue.put(ex);
		} catch (InterruptedException e) {
			// 只抛出异常，不写日志
			throw new DefinedRuntimeException(false, "WriteLog.writeAsyn()异常", e);
		}
	}

	/**
	 * 将异常信息转换为字符串.
	 * @param cause
	 * @return
	 */
	private String getInfo(Throwable cause) {
		String str;
		
		try (StringWriter sw = new StringWriter();
				PrintWriter pw = new PrintWriter(sw)) {

			cause.printStackTrace(pw);
			str = sw.toString();
		} catch (Exception e) {
			str = cause.getClass().getName();
		}

		return str;
	}
	
	/**
	 * 将异常信息转换为字符串.
	 * @param e
	 * @return
	 */
	private String getInfo(DefinedRuntimeException e) {
		String str = e.getMessage();
		str = str == null ? "(null)" : str;

		// 若异常对象中封装了Throwable对象，则取出Throwable对象中的信息
		Throwable cause = e.getCause();
		cause = cause != null ? cause : e;
		str = new StringBuilder(str).append("\r\n").append(getInfo(cause))
				.append(addSuffix ? sdf.format(new Date(e.getOccurrenceTime())) : "").append("\r\n").toString();

		return str;
	}

	/**
	 * 开始循环获取阻塞队列的头部元素并写出到磁盘文件.
	 */
	private void start() {
		try {
			while (run) {
				writeDown(getInfo(queue.take()));
			}
		} catch (Exception e) {
			start();
		}
	}

	/**
	 * 新建文件输出流.
	 * @throws DefinedRuntimeException
	 */
	private void create() throws DefinedRuntimeException {
		try {
			writer = new FileWriter(path, append);
			// 设置为守护线程
			t.setDaemon(true);
			// 设置线程优先级
			t.setPriority(threadPriority);
			t.start();
		} catch (IOException e) {
			// 只抛出异常，不写日志
			throw new DefinedRuntimeException(false, "新建文件输出流异常", e);
		}
	}
	
	/**
	 * 清空队列.
	 */
	public void clear() {
		queue.clear();
	}
	
	/**
	 * 关闭对象.
	 * @throws IOException
	 */
	@Override
	public void close() throws IOException {
		queue.clear();
		// run变量为volatile变量，此处有内存屏蔽
		run = false;
		t.interrupt();
		writer.close();
	}
	
}